package cgl.narada.transport.ipsec;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Properties;
import java.util.StringTokenizer;

import cgl.narada.transport.TransportException;
import cgl.narada.util.ProcessLauncher;

/** 
 * The IPSecTunneler is a class that is responsible for 
 * 1. The creation and set up of IPSec Tunnels
 * 2. The tear down of specific tunnel
 * 3. Dispose all established IPSec Tunnels
 * 
 * @author Shrideep Pallickara
 * @author Craig Shue
 */

public class IPSecTunneler {

    public static final IPSecTunneler instance = new IPSecTunneler();

    public Hashtable tunnels;

    private String policyName = "NaradaBrokering Policy";

    private String ruleName = "NaradaBrokering Rule";

    private String moduleName = "IPSecTunneler: ";

    private IPSecTunneler() {
        tunnels = new Hashtable();
    }

    public static IPSecTunneler getInstance() {
        return instance;
    }

    /**
     * Create an IPSec Tunnel based on the specified hostname
     * 
     * @param tunnelProperties
     * @return The IPSecHostname of the newly created tunnel
     * @throws TransportException
     */
    public String createTunnel(Properties tunnelProperties)
        throws TransportException {
        checkProperties(tunnelProperties);
        String ipSecHostname = getIPSecHostname(tunnelProperties);

        if (tunnels.contains(ipSecHostname)) {
            String report = moduleName + "An IPSecTunnel already exists to"
                + " [" + ipSecHostname
                + "]. No further actions will be performed";
            System.out.println(report);
            return ipSecHostname;
        }

        IPSecTunnelInfo tunnelInfo = createIPSecTunnelInfo(tunnelProperties);
        String commandOne = generateIPSecSetupTunnelCommand(tunnelInfo);
        String commandTwo = "ping " + ipSecHostname;

        establishIPSecTunnel(commandOne, commandTwo);

        tunnels.put(ipSecHostname, tunnelInfo);
        String report = moduleName + "Setting up of the IPSec tunnel to "
            + ipSecHostname + " is now complete!\n";
        System.out.println(report);
        return ipSecHostname;
    }

    /**
     * Deletes an IPSec Tunnel to the specified host
     * 
     * @param hostname
     * @return The IPSecHostname associated with the deleted Tunnel
     * @throws TransportException
     */
    public String deleteTunnel(String hostname) throws TransportException {
        String ipSecHostname = getHostnameAsIPAddress(hostname);

        if (!tunnels.containsKey(ipSecHostname)) {
            String errorReport = moduleName + "An active IPSecTunnel to "
                + ipSecHostname + " does not seem to exist. Try executing: "
                + " <ipseccmd show all> to see the list of all active tunnels";
            reportError(errorReport);
        }
        IPSecTunnelInfo tunnelInfo = (IPSecTunnelInfo) tunnels
            .get(ipSecHostname);
        String teardownCommand = generateIPSecTeardownTunnelCommand(tunnelInfo);

        teardownIPSecTunnel(teardownCommand);
        tunnels.remove(ipSecHostname);
        String report = moduleName + "Teardown of the IPSec tunnel to "
            + ipSecHostname + " is now complete!\n";
        System.out.println(report);
        return ipSecHostname;
    }

    /**
     * Checks to see if a tunnel exists to the specified host. A conversion to
     * the host's IP address is attempted since this conversion is performed
     * before an entry into the list of tunnels.
     * 
     * @param hostname
     * @return
     */
    public boolean tunnelExists(String hostname) {
        String ipHostname = hostname;
        try {
            ipHostname = getHostnameAsIPAddress(hostname);
        } catch (TransportException tranEx) {
            System.out.println(moduleName + "Problems getting IP address for "
                + hostname);
        }

        if (tunnels.contains(ipHostname)) {
            return true;
        }
        return false;
    }

    /**
     * Returns the list of all IP Sec tunnels known to this node.
     * 
     * @return
     */
    public Enumeration getListofIPSecTunnels() {
        return tunnels.elements();
    }

    /** This method disposes all tunnels managed by the IPSecTunneler */
    public void disposeAllTunnels() {
        Enumeration e = tunnels.elements();

        while (e.hasMoreElements()) {

            IPSecTunnelInfo tunnelInfo = (IPSecTunnelInfo) e.nextElement();
            String hostname = tunnelInfo.getHostname();
            try {
                deleteTunnel(hostname);
            } catch (TransportException transEx) {
                String errorReport = moduleName + "Problems closing the IPSec"
                    + " tunnel to [" + hostname + "]\n" + e.toString();
                System.out.println(errorReport);
            }
        }
        /** end while loop */

    }

    /**
     * Executes the IPSec command to set up a tunnel. Also initiates a ping,
     * during which time upto a couple of packets may be lost (this loss is a
     * one-time loss).
     * 
     * @param ipSecCommand
     * @param pingCommand
     * @throws TransportException
     */
    private void establishIPSecTunnel(String ipSecCommand, String pingCommand)
        throws TransportException {

        try {
            System.out.println(moduleName + "Executing IPSecCmd: \n"
                + ipSecCommand);
            ProcessLauncher launcher = ProcessLauncher.getInstance();
            launcher.executeCommand(ipSecCommand);

            launcher.executeCommand(pingCommand);

            String pingReport = moduleName + "Up to 2 ping packets might be"
                + " lost while setting up the IPSec tunnel. From hereon, no"
                + " packets will be lost.";
            System.out.println(pingReport);
        } catch (Exception e) {
            throw new TransportException(e.toString());
        }
    }

    /**
     * Teardown the IPSec Tunnel to a specified host, based on the generated
     * IPSec command.
     * 
     * @param teardownCommand
     * @throws TransportException
     */
    private void teardownIPSecTunnel(String teardownCommand)
        throws TransportException {
        try {
            ProcessLauncher launcher = ProcessLauncher.getInstance();
            launcher.executeCommand(teardownCommand);
        } catch (Exception e) {
            String errorReport = moduleName + "Problems tearing-down IPSec "
                + "Tunnel using the command: " + teardownCommand + "\n" + e;
            reportError(errorReport);
        }
    }

    /**
     * Create an IPSecTunnelInfo object based on the specified properties.
     * 
     * @param tunnelProperties
     * @return
     * @throws TransportException
     */
    private IPSecTunnelInfo createIPSecTunnelInfo(Properties tunnelProperties)
        throws TransportException {
        String errorReport = null;

        String ipSecHostname = getIPSecHostname(tunnelProperties);
        String ipSecPolicyName = tunnelProperties
            .getProperty("IPSecPolicyName", policyName);

        String ipSecRuleName = tunnelProperties.getProperty("IPSecRuleName",
                                                            ruleName);

        String ipSecSharedSecret = tunnelProperties
            .getProperty("IPSecSharedSecret");

        IPSecTunnelInfo tunnelInfo = new IPSecTunnelInfo(ipSecHostname,
                                                         ipSecRuleName,
                                                         ipSecPolicyName,
                                                         ipSecSharedSecret);
        return tunnelInfo;
    }

    /**
     * Create the command for setting up the IPSec Tunnel
     * 
     * @param tunnelInfo
     * @return
     */
    private String generateIPSecSetupTunnelCommand(IPSecTunnelInfo tunnelInfo) {
        String policyName = tunnelInfo.getPolicy();
        String ruleName = tunnelInfo.getRule();
        String sharedSecret = tunnelInfo.getSecret();
        String ipSecHostname = tunnelInfo.getHostname();

        String command = "ipseccmd -w REG -p " + '"' + policyName + '"'
            + " -r " + '"' + ruleName + '"' + " -a p:" + '"' + sharedSecret
            + '"' + " -f 0+" + ipSecHostname + " -n esp[3des,md5] -x";

        return command;
    }

    /**
     * Create the command for tearing down an existing IPSec Tunnel
     * 
     * @param tunnelInfo
     * @return
     */
    private String generateIPSecTeardownTunnelCommand(IPSecTunnelInfo tunnelInfo) {
        String policyName = tunnelInfo.getPolicy();
        String ruleName = tunnelInfo.getRule();

        String command = "ipseccmd -w REG -p " + '"' + policyName + '"'
            + " -r " + '"' + ruleName + '"' + " -y";

        return command;
    }

    /**
     * Checks to the see if the specified properties are NULL, and reports
     * problems if it is.
     * 
     * @param tunnelProperties
     * @throws TransportException
     */
    private void checkProperties(Properties tunnelProperties)
        throws TransportException {
        String errorReport = null;
        if (tunnelProperties == null) {
            errorReport = moduleName + "The properties specified for the "
                + "IPSec tunnel are NULL";
            reportError(errorReport);
        }
    }

    /**
     * Retrieves the IPSecHostname from the specified set of properties
     * 
     * @param tunnelProperties
     * @return
     * @throws TransportException
     *             if there are problems retrieving the IPSecHostname property
     */
    private String getIPSecHostname(Properties tunnelProperties)
        throws TransportException {
        String errorReport = null;
        String ipSecHostname = tunnelProperties.getProperty("IPSecHostname");

        if (ipSecHostname == null) {
            errorReport = moduleName + "The IPSecHostname is not specified!";
            reportError(errorReport);
        }

        ipSecHostname = getHostnameAsIPAddress(ipSecHostname);
        return ipSecHostname;
    }

    /**
     * Retrieve the raw IP Address of the specified host.
     * 
     * @param hostname
     * @return
     * @throws TransportException
     */
    private String getHostnameAsIPAddress(String hostname)
        throws TransportException {
        String ipSecHostname = null;
        try {
            InetAddress ia = InetAddress.getByName(hostname);
            ipSecHostname = ia.getHostAddress();

        } catch (UnknownHostException e) {
            String errorReport = moduleName + "Unable to resolve host: "
                + hostname;
            reportError(errorReport);
        }
        return ipSecHostname;
    }

    /**
     * Throws a TransportException with the specified error Report
     * 
     * @param errorReport
     * @throws TransportException
     */
    private void reportError(String errorReport) throws TransportException {
        if (errorReport != null) {
            throw new TransportException(errorReport);
        }

    }

    /**
     * Main method
     * 
     * @param args
     */
    public static void main(String[] args) {
        IPSecTunneler tunneler = IPSecTunneler.getInstance();
        String module = "IPSecTunneler.main() -> ";
        String helpInfo = "\nIPSecTunneler can be used to setup/teardown IPSec "
            + " tunnels. The commands, and the corresponding arguments they"
            + "take, are listed below. Arguments should be specified in order "
            + "and without the <> brackets:\n\n"
            + "s <hostname> <policy_name> <rule_name> <secret>: "
            + "SETUP IPSec Tunnel\n"
            + "t <ipsec_hostname>                             : "
            + "TEARDOWN IPSec Tunnel\n"
            + "d                                              : "
            + "Dispose ALL IPSec Tunnels\n"
            + "q                                              : "
            + "Quit the system\n";
        System.out.println(helpInfo);
        InputStreamReader isr = new InputStreamReader(System.in);
        BufferedReader commandLine = new BufferedReader(isr);
        StringTokenizer tokenizer = null;
        try {
            while (true) {
                String command;

                command = commandLine.readLine();

                if (command == null) {
                    System.out.println(module + "String is null!!!");
                    break;
                }

                if (command.equals("h")) {
                    System.out.println(helpInfo);
                }

                if (command.equals("q")) {
                    System.out.print(module + "Preparing to quit the program");
                    break;
                }

                if (command.startsWith("s")) {
                    tokenizer = new StringTokenizer(command, " ");
                    if (tokenizer.countTokens() != 5) {
                        String errorReport = module + "Please make sure that "
                            + "you specify all arguments. The correct command "
                            + "for setting up the IPSec Tunnel is:\n\n\t"
                            + "t <hostname> <policy_name> <rule_name> <secret>"
                            + "\n";
                        System.out.println(errorReport);
                        continue;
                    }
                    Properties props = new Properties();
                    tokenizer.nextToken();
                    props.put("IPSecHostname", tokenizer.nextToken());
                    props.put("IPSecPolicyName", tokenizer.nextToken());
                    props.put("IPSecRuleName", tokenizer.nextToken());
                    props.put("IPSecSharedSecret", tokenizer.nextToken());
                    try {
//                        IPSecTunnelInfo tunnelInfo = tunneler
//                            .createIPSecTunnelInfo(props);
//                        String command = tunneler
//                            .generateIPSecSetupTunnelCommand(tunnelInfo);
//                        System.out.println(command);
                        tunneler.createTunnel(props);
                    } catch (TransportException e) {
                        System.out.println(e);
                    }
                    continue;
                } /** end creation of tunnel */

                
                if (command.startsWith("t")) {
                    tokenizer = new StringTokenizer(command, " ");
                    if (tokenizer.countTokens() != 2) {
                        String errorReport = module + "Please make sure that "
                            + "you specify all arguments. The correct command "
                            + "for tearing down the IPSec Tunnel is:\n\n\t"
                            + "t <hostname> \n";
                        System.out.println(errorReport);
                        continue;
                    }
                    tokenizer.nextToken();
                    try {
                        tunneler.deleteTunnel(tokenizer.nextToken());
                    } catch (TransportException e) {
                        System.out.println(e);
                    }
                    continue;
                } /*end teardown of the IPSec tunnels */
                
                if (command.equals("d")) {
                    tunneler.disposeAllTunnels();
                }
                
                
            }
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } /* end try-catch */

    }
}
